﻿namespace Webinator.Util
{
    using System;
    using System.Diagnostics.CodeAnalysis;

    /// <summary>
    /// Uses a Color Lookup Table to compare colors.
    /// </summary>
    internal class ColorComparer
    {
        /// <summary>
        /// This is the CLUT - it costs about 16mb, but once initialized, the performance goes through the roof 
        /// </summary>
        private static readonly byte[,,] _labclut = new byte[256, 256, 256];

        /// <summary>
        /// Indicates whether the CLUT has been intialized.
        /// </summary>
        private static bool _init;

        /// <summary>
        /// Initializes the CLUT.
        /// </summary>
        public static void InitializeLut()
        {
            if (!_init)
            {
                for (int rr = 0; rr < 256; rr++)
                {
                    for (int gg = 0; gg < 256; gg++)
                    {
                        for (int bb = 0; bb < 256; bb++)
                        {
                            var lab = RgbToLab(rr, gg, bb);
                            _labclut[rr, gg, bb] = (byte)Math.Abs(Math.Round(Math.Abs(lab.L) + Math.Abs(lab.A) + Math.Abs(lab.B), 0));
                        }
                    }
                }

                _init = true;
            }
        }

        /// <summary>
        /// Calculates the difference between two RGB values.
        /// </summary>
        /// <param name="r">
        /// The r.
        /// </param>
        /// <param name="g">
        /// The g.
        /// </param>
        /// <param name="b">
        /// The b.
        /// </param>
        /// <param name="rr">
        /// The rr.
        /// </param>
        /// <param name="gg">
        /// The gg.
        /// </param>
        /// <param name="bb">
        /// The bb.
        /// </param>
        /// <returns>
        /// The difference between the two RGB values.
        /// </returns>
        public static double ColorDifferenceLab(int r, int g, int b, int rr, int gg, int bb)
        {
            return Math.Abs(LookupTableLab(r, g, b) - LookupTableLab(rr, gg, bb));
        }

        /// <summary>
        /// Gets the LAB value for the given RGB value.
        /// </summary>
        /// <param name="r">
        /// The r.
        /// </param>
        /// <param name="g">
        /// The g.
        /// </param>
        /// <param name="b">
        /// The b.
        /// </param>
        /// <returns>
        /// The LAB value.
        /// </returns>
        private static double LookupTableLab(int r, int g, int b)
        {
            return _labclut[r, g, b];
        }

        /// <summary>
        /// Converts RGB to CIE XYZ (CIE 1931 color space)
        /// </summary>
        /// <param name="red">
        /// Red must be in [0, 255].
        /// </param>
        /// <param name="green">
        /// Green must be in [0, 255].
        /// </param>
        /// <param name="blue">
        /// Blue must be in [0, 255].
        /// </param>
        /// <returns>
        /// A <c>CIEXYZ</c> object.
        /// </returns>
        private static CieXyz RgbToXyz(int red, int green, int blue)
        {
            // normalize red, green, blue values
            double rLinear = red / 255.0;
            double gLinear = green / 255.0;
            double bLinear = blue / 255.0;

            // convert to a sRGB form
            double r = (rLinear > 0.04045) ? Math.Pow((rLinear + 0.055) / (1 + 0.055), 2.2) : (rLinear / 12.92);
            double g = (gLinear > 0.04045) ? Math.Pow((gLinear + 0.055) / (1 + 0.055), 2.2) : (gLinear / 12.92);
            double b = (bLinear > 0.04045) ? Math.Pow((bLinear + 0.055) / (1 + 0.055), 2.2) : (bLinear / 12.92);

            // converts
            return new CieXyz(
                (r * 0.4124 + g * 0.3576 + b * 0.1805),
                (r * 0.2126 + g * 0.7152 + b * 0.0722),
                (r * 0.0193 + g * 0.1192 + b * 0.9505));
        }

        /// <summary>
        /// Converts RGB to CIELab.
        /// </summary>
        /// <param name="red">
        /// The red.
        /// </param>
        /// <param name="green">
        /// The green.
        /// </param>
        /// <param name="blue">
        /// The blue.
        /// </param>
        /// <returns>
        /// A <c>CIELab</c> object.
        /// </returns>
        private static CieLab RgbToLab(int red, int green, int blue)
        {
            var xyz = RgbToXyz(red, green, blue);
            return XyzToLab(xyz.X, xyz.Y, xyz.Z);
        }

        /// <summary>
        /// XYZ to L*a*b* transformation function.
        /// </summary>
        /// <param name="t">The value to transform.</param>
        /// <returns>A <c>double</c>.</returns>
        private static double Fxyz(double t)
        {
            return (t > 0.008856) ? Math.Pow(t, (1.0 / 3.0)) : (7.787 * t + 16.0 / 116.0);
        }

        /// <summary>
        /// Converts CIEXYZ to CIELab structure.
        /// </summary>
        /// <param name="x">
        /// The x.
        /// </param>
        /// <param name="y">
        /// The y.
        /// </param>
        /// <param name="z">
        /// The z.
        /// </param>
        /// <returns>
        /// A <c>CIELab</c> object.
        /// </returns>
        private static CieLab XyzToLab(double x, double y, double z)
        {
            CieLab lab = new CieLab();

            lab.L = 116.0 * Fxyz(y / 1.0) - 16;
            lab.A = 500.0 * (Fxyz(x / 0.9505) - Fxyz(y / 1.0));
            lab.B = 200.0 * (Fxyz(y / 1.0) - Fxyz(z / 1.089));

            return lab;
        }

        /// <summary>
        /// Class to define CIE XYZ.
        /// </summary>
        [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
        public class CieXyz
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="CieXyz"/> class.
            /// </summary>
            /// <param name="x">
            /// The x.
            /// </param>
            /// <param name="y">
            /// The y.
            /// </param>
            /// <param name="z">
            /// The z.
            /// </param>
            public CieXyz(double x, double y, double z)
            {
                X = x;
                Y = y;
                Z = z;
            }

            public double X { get; set; }

            public double Y { get; set; }

            public double Z { get; set; }
        }

        /// <summary>
        /// Class to define CIE L*a*b*.
        /// </summary>
        [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
        public class CieLab
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="CieLab"/> class.
            /// </summary>
            public CieLab()
            {
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="CieLab"/> class.
            /// </summary>
            /// <param name="l">
            /// The l.
            /// </param>
            /// <param name="a">
            /// The a.
            /// </param>
            /// <param name="b">
            /// The b.
            /// </param>
            public CieLab(double l, double a, double b)
            {
                L = l;
                A = a;
                B = b;
            }

            public double L { get; set; }

            public double A { get; set; }

            public double B { get; set; }
        }
    }
}